﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Logging;
using Roslyn.Compilers;
using Roslyn.Compilers.CSharp;
using CompleterLib.Util;
using Util;

namespace CompleterLib
{
    public class MethodCache
    {
        private static List<MethodSymbol> allNonLocalStaticMethods;
        private static readonly Dictionary<string, MethodCache> methodCaches =
            new Dictionary<string, MethodCache>();

        private readonly TypeSymbol forType;
        private readonly string typeName;
        private readonly int staticCount;
        private readonly MethodCache[] parents;
        private readonly List<MethodSymbol> staticMethods;

        private MethodCache(TypeSymbol forType, SemanticModel semModel)
        {
            var start = DateTime.Now;

            this.forType = forType;
            this.typeName = this.forType.ToDisplayString();

            var parentTypes = this.forType.GetOnlySuperTypes().ToList();
            if (!parentTypes.Any() && !forType.SpecialType.Equals(SpecialType.System_Object))
            {
                parentTypes = semModel.Compilation.GetSpecialType(SpecialType.System_Object)
                    .SingletonList<TypeSymbol>();
            }
            // TODO Complete assignability for primitive types.
            if (forType.SpecialType.Equals(SpecialType.System_Byte))
            {
                parentTypes.AddRange(new SpecialType[] {
                    SpecialType.System_Int16,
                    SpecialType.System_Int32,
                    SpecialType.System_Int64,
                    SpecialType.System_UInt16,
                    SpecialType.System_UInt32,
                    SpecialType.System_UInt64,
                    }.Select(special =>
                        semModel.Compilation.GetSpecialType(special)));
            }
            else if (forType.SpecialType.Equals(SpecialType.System_Int16))
            {
                parentTypes.AddRange(new SpecialType[] {
                    SpecialType.System_Int32,
                    SpecialType.System_Int64,
                    SpecialType.System_UInt32,
                    SpecialType.System_UInt64,
                    }.Select(special =>
                        semModel.Compilation.GetSpecialType(special)));
            }
            else if (forType.SpecialType.Equals(SpecialType.System_Int32))
            {
                parentTypes.AddRange(new SpecialType[] {
                    SpecialType.System_Int64,
                    SpecialType.System_UInt64,
                    }.Select(special =>
                        semModel.Compilation.GetSpecialType(special)));
            }
            else if (forType.SpecialType.Equals(SpecialType.System_Char))
            {
                parentTypes.AddRange(new SpecialType[] {
                    SpecialType.System_Int32,
                    SpecialType.System_Int64,
                    SpecialType.System_UInt32,
                    SpecialType.System_UInt64,
                    }.Select(special =>
                        semModel.Compilation.GetSpecialType(special)));
            }
            else if (forType.SpecialType.Equals(SpecialType.System_Single))
            {
                parentTypes.AddRange(new SpecialType[] {
                    SpecialType.System_Double,
                    }.Select(special =>
                        semModel.Compilation.GetSpecialType(special)));
            }
            this.parents = parentTypes
                .Select(t => GetMethodCacheForType(t, semModel))
                .Distinct()
                .ToArray();
            this.staticMethods = allNonLocalStaticMethods
                .Where(m => m.Parameters.Select(p => p.Type)
                             .Any(p => p.IsSameType(this.forType, semModel)))
                .ToList();
            this.staticCount = staticMethods.Count
                + AllParents.Select(p => p.staticMethods.Count).Sum();

            Logger.PrintLine("Time to generate method cache: "
                + (DateTime.Now - start).TotalMilliseconds
                + " ms for " + this.ToString());

            // XXX DEBUG
            this.EnsureNotInParents();
        }
        private HashSet<MethodCache> AllParents
        {
            get
            {
                HashSet<MethodCache> res = new HashSet<MethodCache>(parents);
                foreach (var p in parents)
                {
                    res.UnionWith(p.AllParents);
                }
                return res;
            }
        }
        public static MethodCache GetMethodCacheForType(TypeSymbol type, SemanticModel semModel)
        {
            MethodCache res;
            string typeStr = type.ToDisplayString();
            if (!methodCaches.TryGetValue(typeStr, out res))
            {
                res = new MethodCache(type, semModel);
                methodCaches[res.typeName] = res;
            }
            return res;
        }

        private void EnsureNotInParents()
        {
            foreach (var p in parents)
            {
                p.EnsureNotInParents(this.typeName);
            }
        }
        private void EnsureNotInParents(string typeName)
        {
            if (this.typeName.Equals(typeName))
            {
                throw new Exception("Loop with type " + typeName);
            }
            foreach (var p in parents)
            {
                p.EnsureNotInParents(typeName);
            }
        }

        public IEnumerable<MethodSymbol> AllStaticMethods
        {
            get
            {
                return staticMethods.Concat(AllParents.Select(p => p.staticMethods).Concat());
            }
        }
        public int StaticCount
        {
            get
            {
                return this.staticCount;
            }
        }

        public override string ToString()
        {
            return "MethodCache for " + typeName
                + " with " + staticMethods.Count + " static methods "
                + "(" + staticCount + " total static methods)"
                //+ " and " + instanceMethods.Count + " instance methods "
                //+ "(" + instanceCount + " total instance methods)"
                + " and " + parents.Length + " supertypes";
        }

        public override int GetHashCode()
        {
            return typeName.GetHashCode();
        }
        public override bool Equals(object obj)
        {
            return obj == this;
        }

        private static SortedSet<string> references = null;
        public static void SignalNewSemanticModel(SemanticModel semModel, CancellationToken token)
        {
            var editingReferences = new SortedSet<string>(semModel.Compilation.References.Select(r => r is AssemblyFileReference ? (r as AssemblyFileReference).Path : r.ToString()));
            if (references == null || !editingReferences.SetEquals(references))
            {
                // different references, delete everything
                methodCaches.Clear();
                AbstractTypes.AbstractTypeInferencer.ClearExternalAbsTypes(semModel);
                allNonLocalStaticMethods = null;
                references = editingReferences;
                allNonLocalStaticMethods = semModel.GetAllStaticMethods()
                    .Where(meth => !meth.ContainingAssembly.Equals(semModel.Compilation.Assembly)
                        && (meth.CanBeReferencedByName || meth.MethodKind.Equals(MethodKind.Constructor)))
                    .ToList();
            }
        }
        public static void SignalFinishedWithSemanticModel(SemanticModel semModel)
        {
            var editingAssembly = semModel.Compilation.Assembly;
            var obsoleteCaches = methodCaches
                .Where(entry => entry.Value.forType.ContainingAssembly != null
                    && entry.Value.forType.ContainingAssembly.Equals(editingAssembly))
                .Select(entry => entry.Key)
                .ToList();
            foreach (var typeStr in obsoleteCaches)
            {
                methodCaches.Remove(typeStr);
            }
        }
    }

    public static class MethodCacheUtil
    {
        public static MethodCache GetSmallestMethodCacheForTypes(
            IEnumerable<TypeSymbol> types, SemanticModel semModel)
        {
            return types
                .Select(t => MethodCache.GetMethodCacheForType(t, semModel))
                .ArgMin(c => c.StaticCount);
        }
    }
}
